伴随Simplex噪声的GPU粒子动画
我们将继续分享加拿大游戏特效大神Mirza Beig的粒子系统的系列教程,我们将在上一篇《Unity自定义粒子顶点流》的基础上,学习在Unity中实现伴随Simplex噪声的GPU粒子动画。
在《Unity自定义粒子顶点流》教程中,我们学习了如何制作基础的粒子着色器,让它读取和使用从正确配置的粒子系统发送的顶点流,最后我们得到了富有弹性的弹簧粒子效果,该效果不是很有创意,也并不惊艳。
本教程中,我们将在此基础上更进一步,实现下图的效果。
如果你已经完成了上一个的教程,并拥有《创建3D均匀粒子网格》教程的脚本,那么本教程中特效的制作过程会非常简单。
本教程中我们会对每个粒子的中心传递额外的3D顶点流,然后使用该顶点流决定来自噪声函数的顶点偏移。本文将使用的噪声函数为Simplex噪声,类似柏林噪声。
Part 1:粒子系统设置
首先设置一个基础粒子系统,我们可以用它来测试效果。我们将使用默认粒子纹理,最后的结果类似下图。
首先,创建一个粒子系统。
重置Transform组件,然后按下图设置Main模块。勾选了Prewarm,设置Start Speed为0,并将Start Size设为在0.25和1之间随机取值,然后设置Max Particles为10,000。
设置Emission模块的Rate over Time为2,000。
将Shape设为Box,Scale设为[50, 0, 50]。
现在我们应该会得到一个反复浮动的平坦粒子层。
我们需要使生成效果更平滑,现在启用Color over Lifetime模块,按照下图设置梯度,从而浅入淡出粒子。
在Renderer模块中,启用Custom Vertex Streams并添加Center流。
这样会造成TEXCOORD1的溢出,我们会在处理着色器时解决该问题,这样就完成了测试粒子系统。
Part 2:基本着色器
在这一部分,我们将创建基本着色器,用来处理额外的顶点流数据。该着色器将基于我们在《Unity自定义粒子顶点流》中创建的基础无光粒子着色器,下面的代码以供参考。
Shader "Unlit/Simple Particle Unlit"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
LOD 100
Blend One One // 加法混合
ZWrite Off // 关闭深度测试
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 实现模糊效果
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float3 tc0 : TEXCOORD0;
};
struct v2f
{
float3 tc0 : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert(appdata v)
{
v2f o;
float3 vertexOffset = 0;
v.vertex.xyz += vertexOffset;
o.vertex = UnityObjectToClipPos(v.vertex);
//从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色
o.color = v.color;
o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex);
//初始化tex coord变量
o.tc0.z = v.tc0.z;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
// 采样纹理
fixed4 col = tex2D(_MainTex, i.tc0);
// 让纹理颜色和粒子系统的顶点颜色输入相乘.
col *= i.color;
col *= col.a;
// 应用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
我们需要修改一部分内容,首先是名称。
Shader "Unlit/Simplex Noise Particle Unlit"
将tc0之前为uv,定义为float4,并添加TEXCOORD1为tc1,。这样允许我们通过使用该着色器,传入并保存更多来自粒子系统的自定义顶点流数据。
struct appdata
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float4 tc0 : TEXCOORD0;
float4 tc1 : TEXCOORD1;
};
struct v2f
{
float4 tc0 : TEXCOORD0;
float4 tc1 : TEXCOORD1;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
};
接下来,我们初始化导出的tc0.zw到tc0.zw输入中的内容。
我们指定了z和w,因为xy已经被指定为纹理的UV坐标。我们可以将整个tc1输入转储到tc1输出,因为在顶点程序中,这部分数据不和之前的任何操作共享。
v2f vert (appdata v)
{
v2f o;
float3 vertexOffset = 0;
v.vertex.xyz += vertexOffset;
o.vertex = UnityObjectToClipPos(v.vertex);
// 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色
o.color = v.color;
o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex);
// 初始化tex coord变量
o.tc0.zw = v.tc0.zw;
o.tc1 = v.tc1;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
我们可以使用该着色器处理上一部分使用的粒子系统,而且不必看到烦人的不匹配流警告。
我们返回Unity编辑器中,创建一个新材质,为其指定着色器,然后设置粒子系统Renderer模块的材质。指定Unity的默认粒子纹理。
如果一切顺利,我们应该不会看到任何警告,而且粒子系统会按照之前的方法渲染。
下面是完整的基本着色器代码。
Shader "Unlit/Simplex Noise Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
LOD 100
Blend One One // 加法混合
ZWrite Off // 关闭深度测试
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 实现模糊效果
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float4 tc0 : TEXCOORD0;
float4 tc1 : TEXCOORD1;
};
struct v2f
{
float4 tc0 : TEXCOORD0;
float4 tc1 : TEXCOORD1;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
float3 vertexOffset = 0;
v.vertex.xyz += vertexOffset;
o.vertex = UnityObjectToClipPos(v.vertex);
// 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色。
o.color = v.color;
o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex);
// 初始化tex coord变量
o.tc0.zw = v.tc0.zw;
o.tc1 = v.tc1;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 采样纹理
fixed4 col = tex2D(_MainTex, i.tc0);
// 让纹理颜色和粒子系统的顶点颜色输入相乘
col *= i.color;
col *= col.a;
// 应用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 3:Simplex噪声(顶点动画)
这一部分是本教程的重点。我们将在此使用Keijiro Takahashi的HLSL实现方法在Unity实现Simplex噪声。
请访问GitHub下载代码库,它包含多个不同的噪声函数,但我们只需要3D Simplex噪声,即SimplexNoise3D.hlsl。
下载噪声代码库:
https://github.com/keijiro/NoiseShader/blob/master/Assets/HLSL/SimplexNoise3D.hlsl
为了能够在着色器中使用噪声函数,我们需要通过include引用该代码。
#include "UnityCG.cginc"
#include "SimplexNoise3D.hlsl"
我们将添加一些参数,用来控制噪声速度,即在3D空间中滚动偏移的速度、频率、振幅和负范围限定。我们将这些参数作为属性添加到着色器中。
_MainTex ("Texture", 2D) = "white" {}
_NoiseSpeedX("Noise Speed X", Range(0 , 100)) = 0.0
_NoiseSpeedY("Noise Speed Y", Range(0 , 100)) = 0.0
_NoiseSpeedZ("Noise Speed Z", Range(0 , 100)) = 1.0
_NoiseFrequency("Noise Frequency", Range(0 , 1)) = 0.1
_NoiseAmplitude("Noise Amplitude", Range(0 , 10)) = 2.0
_NoiseAbs("Noise Abs", Range(0 , 1)) = 1.0
我们还需要添加匹配的着色器变量。
sampler2D _MainTex;
float4 _MainTex_ST;
float _NoiseSpeedX;
float _NoiseSpeedY;
float _NoiseSpeedZ;
float _NoiseFrequency;
float _NoiseAmplitude;
float _NoiseAbs;
现在我们可以处理顶点部分即在添加偏移前,从而为粒子设置动画。请记住,粒子的中心向量在TEXCOORD0和TEXCOORD1中混合。
中心向量会转换为存在tc0.zw的中心X和Y,以及存在tc0.x的Z。你可能会想,如果我们要像之前教程那样偏移顶点,为什么我们还要传入Center顶点流?
这是因为粒子顶点会根据粒子的世界空间位置在最初被偏移,然后用作噪声函数的输入。随着大量粒子在足够大的区域中扩散,我们可以看到平滑的噪声分布,并使它随着时间变化。
我们不能直接传入顶点位置,因为粒子是带有至少三个顶点的多边形,每个顶点都是空间中的不同点。如果我们使用了顶点,那么我们要根据顶点获取不同的偏移,这样在网格变形时,会得到非常奇怪的粒子。
这不是我们想要的效果,我们希望整个粒子进行移动,这意味着要以相同的偏移来一起移动它的所有顶点,所以我们使用粒子的中心位置。
float3 particleCenter = float3(v.tc0.zw, v.tc1.x);
我们可以通过time * speedZ计算出3D噪声偏移。虽然我们可以为所有轴添加单独的噪声速度,但我们不想让噪声在X轴和Y轴变化。
float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);
现在我们可以计算单维噪声值。首先传入particleCenter,添加偏移,然后将结果乘以频率属性。
float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);
snoise函数会提供范围在[-1.0, 1.0]的数值。因为我们想要将该数值重映射为[0.0, 1.0]的范围,然后在范围间进行混合,所以我们可以添加代码来实现这个过程,代码首先会重新映射,然后使用lerp函数来进行混合。
float noise01 = (noise + 1.0) / 2.0;
float noiseRemap = lerp(noise, noise01, _NoiseAbs);
顶点偏移会在世界空间Y中计算noiseRemap * _NoiseAmplitude 。
float3 vertexOffset = float3(0.0, noiseRemap * _NoiseAmplitude, 0.0);
v.vertex.xyz += vertexOffset;
顶点动画的处理到这就完成了,下一部分我们将会根据噪声值设置颜色的变化。我们返回到Unity中查看结果,下面的示例效果可以通过调整粒子系统Main模块的Start Color来实现。
Part4:Simplex噪声(片段/像素颜色动画)
我们已经拥有代码来根据噪声制作顶点动画,所以这部分差不多就要完成了。首先添加二个额外属性和全局变量,用于根据重新映射的[0.0, 1.0]范围噪声值来处理插补的颜色。
材质属性会添加到着色器文件的顶部,,HDR标签能确保我们可以大幅增大进入HDR范围的强度,从而允许泛光等后期处理效果能和着色器搭配使用。
[HDR] _ColourA("Color A", Color) = (0,0,0,0)
[HDR] _ColourB("Color B", Color) = (1,1,1,1)
着色器变量添加在噪声变量下。
float4 _ColourA;
float4 _ColourB;
最后在片段部分,在将col乘以输入顶点颜色的代码和将col乘以结合Alpha值的代码之间,添加下面的代码。
col *= i.color;
float3 particleCenter = float3(i.tc0.zw, i.tc1.x);
float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);
float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);
float noise01 = (noise + 1.0) / 2.0;
col = lerp(col * _ColourA, col * _ColourB, noise01);
col *= col.a;
你可以注意到,大部分代码是从顶点部分复制粘贴得来,保存脚本,使用i.tc0和i.tc1来从输入获取流,并使用lerp函数来在颜色间插补。
这些代码只是少量修改了顶点动画部分的代码,下面是得到的效果。
这样我们就处理好着色器了,在下一部分,我们将使用现有的代码实现预览图像。下面是完整的着色器代码。
Shader "Unlit/Simplex Noise Particle Unlit"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_NoiseSpeedX("Noise Speed X", Range(0 , 100)) = 0.0
_NoiseSpeedY("Noise Speed Y", Range(0 , 100)) = 0.0
_NoiseSpeedZ("Noise Speed Z", Range(0 , 100)) = 1.0
_NoiseFrequency("Noise Frequency", Range(0 , 1)) = 0.1
_NoiseAmplitude("Noise Amplitude", Range(0 , 10)) = 2.0
_NoiseAbs("Noise Abs", Range(0 , 1)) = 1.0
[HDR] _ColourA("Color A", Color) = (0,0,0,0)
[HDR] _ColourB("Color B", Color) = (1,1,1,1)
}
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
LOD 100
Blend One One // 加法混合
ZWrite Off // 关闭深度测试
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 实现模糊效果
#pragma multi_compile_fog
#include "UnityCG.cginc"
#include "SimplexNoise3D.hlsl"
struct appdata
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float4 tc0 : TEXCOORD0;
float4 tc1 : TEXCOORD1;
};
struct v2f
{
float4 tc0 : TEXCOORD0;
float4 tc1 : TEXCOORD1;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _NoiseSpeedX;
float _NoiseSpeedY;
float _NoiseSpeedZ;
float _NoiseFrequency;
float _NoiseAmplitude;
float _NoiseAbs;
float4 _ColourA;
float4 _ColourB;
v2f vert (appdata v)
{
v2f o;
float3 particleCenter = float3(v.tc0.zw, v.tc1.x);
float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);
float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);
float noise01 = (noise + 1.0) / 2.0;
float noiseRemap = lerp(noise, noise01, _NoiseAbs);
float3 vertexOffset = float3(0.0, noiseRemap * _NoiseAmplitude, 0.0);
v.vertex.xyz += vertexOffset;
o.vertex = UnityObjectToClipPos(v.vertex);
// 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色
o.color = v.color;
o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex);
// 初始化tex coord变量
o.tc0.zw = v.tc0.zw;
o.tc1 = v.tc1;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//采样纹理
fixed4 col = tex2D(_MainTex, i.tc0);
//让纹理颜色和粒子系统的顶点颜色输入相乘
col *= i.color;
float3 particleCenter = float3(i.tc0.zw, i.tc1.x);
float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);
float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);
float noise01 = (noise + 1.0) / 2.0;
col = lerp(col * _ColourA, col * _ColourB, noise01);
col *= col.a;
// 应用模糊效果
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
Part 5:组合所有内容
最后这一部分,我们需要使用《创建3D均匀粒子网格》教程中的脚本。我们将创建三个粒子系统,每个都有不同的材质和设置,以及相同的纹理。
三个粒子系统都将添加粒子网格脚本,设置如下图所示。
除了Renderer模块外,所有模块都要禁用。发射,颜色和形状都由附加的脚本控制。
所有粒子系统Main模块的设置如下。即Start Lifetime设为1,000,Start Speed设为0,Start Size设为0.5,Max Particles设为100,000。
确保启用Custom Vertex Streams,并添加Center流。
粒子系统1
粒子系统1是在背景向下滚动的绿色网格。
Transform组件按下图进行设置,Position Z设为10,Rotation X为110,Scale设为2, 1, 1.5。
Noise Speed的XYZ值分别为(0, 1, 0),Noise Frequency为0.25,Noise Amplitude为0.75,Noise Abs为1。二个颜色分别为蓝色(Alpha = 10)和绿色(Alpha = 75)。
粒子系统2
粒子系统2是发光的红色3D波形层。
按照下图设置Transform组件,Position的XYZ值分别为(0, -1.25, 8),Rotation的XYZ值都为0,Scale的XYZ值分别为(3, 0.5, 1)。
设置Render Mode为Mesh,然后设置Mesh为Cube。
该系统材质的唯一变化为颜色。颜色都为红色,Alpha值分别为0和25。
粒子系统3
粒子系统3是粒子系统2的副本,我们会再次使用立方体网格发射器来创建蓝色的波形层。
我们需要设置Transform属性,Position的XYZ值分别为(0, -3.15, 10)。Rotation的XYZ值为0。Scale的XYZ值分别为(12, 1, 1)。
最后,我们需要修改材质颜色为深蓝色和湖绿色,不透明度分别为5和25。
最终效果
现在完成了整个教程,我们会得到类似下图的效果。
小结
在Unity中实现伴随Simplex噪声的GPU粒子动画就学习到这里,虽然效果依旧不是特别精美,但是这为后续教程中实现更为复杂的精美粒子特效奠定基础,也帮助我们熟练掌握噪音在粒子系统中的使用。
更多Unity教程,尽在Unity官方中文论坛(UnityChina.cn)。
原文来源:mirzabeig.com
粒子特效教程
官方活动
2019年5月10日-12日上海,Unite大会强势回归。 暖冬特惠票正式开售,购票即获指定Asset Store资源商店精品21款资源的5折优惠券。[了解详情...]
购票链接:
http://UniteShanghai2019.bagevent.com
Unity全球学生开发挑战赛火热进行中(最后一周)
Unity面向全球的学生推出-Unity全球学生开发挑战赛,寻找全世界最具创意,展现自我的学生开发者团队。[了解详情...]
活动地址:
https://connect.unity.com/challenges/gsc2018
活动 | Unity for Humanity 2019挑战赛
参加Unity for Humanity挑战赛,向我们展示你的作品将如何改变世界。[了解详情...]
活动地址:
https://connect.unity.com/challenges/unityforhumanity
点击“阅读原文”访问Unity官方中文论坛